package com.zhang.common.jpa.specification;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;

import javax.persistence.criteria.*;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.SingularAttribute;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;

public class EntitySpecification<T> implements Specification<T> {

    /**模糊匹配前缀*/
    private static final String LIKE_PREFIX="LIKE1";
    /**模糊匹配--忽略大小写*/
    private static final String LIKE_IGNORE_CASE_PREFIX="LIKE2";
    /**模糊匹配后缀*/
    private static final String LIKE_SUFFIX="@@";
    /**区间查询开始后缀*/
    private static final String INTERVAL_SUFFIX_BEGIN="BEGIN";
    /**区间查询结束后缀*/
    private static final String INTERVAL_SUFFIX_END="END";
    /**时间格式化 */
    private String dateFormat;
    private static Map<Class<?>, String> DEFAULT_DATE_FORMATS = new HashMap<>();

    static {
        DEFAULT_DATE_FORMATS.put(Date.class, "yyyy-MM-dd HH:mm:ss");
        DEFAULT_DATE_FORMATS.put(LocalDate.class, "yyyy-MM-dd");
        DEFAULT_DATE_FORMATS.put(LocalDateTime.class, "yyyy-MM-dd\'T\'HH:mm:ss");
        DEFAULT_DATE_FORMATS.put(OffsetDateTime.class, "yyyy-MM-dd\'T\'HH:mm:ss.SSSXXX");
        DEFAULT_DATE_FORMATS.put(Instant.class, "yyyy-MM-dd\'T\'HH:mm:ss.SSSXXX");
    }
    /**要进行查询的对象*/
    T searchObj;
    /**携带的除实体类对象外的额外参数 例如区间查询参数*/
    Map<String, String[]> parameterMap;




    public EntitySpecification(T searchObj, Map<String, String[]> parameterMap) {
        this.searchObj = searchObj;
        this.parameterMap = parameterMap;
    }

    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {

        //存放Predicate的集合
        List<Predicate> predicates=new ArrayList<>();
        //查询实体类型
        EntityType<T> type = root.getModel();
        //对象属性Wrapper
        DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(searchObj);

        //传入对象的等值和字符串模糊匹配查询 属性遍历===>>begin
        for (SingularAttribute attribute : type.getSingularAttributes()) {
            //变量名
            String attributeName = attribute.getName();
            Class javaType = attribute.getJavaType();


            //大于等于查询
            if (parameterMap.containsKey(attributeName+INTERVAL_SUFFIX_BEGIN)){
                Expression<? extends Comparable> expression = root.get(attribute);
                Object object = convert(parameterMap.get(attributeName + INTERVAL_SUFFIX_BEGIN)[0], javaType);
                predicates.add(cb.greaterThanOrEqualTo(expression,(Comparable)object));
            }
            //小于等于查询
            if (parameterMap.containsKey(attributeName+INTERVAL_SUFFIX_END)){
                Expression<? extends Comparable> expression = root.get(attribute);
                Object object = convert(parameterMap.get(attributeName + INTERVAL_SUFFIX_END)[0], javaType);
                predicates.add(cb.lessThanOrEqualTo(expression,(Comparable)object));
            }



            //取得当前的属性值
            Optional<Object> optionalValue = Optional.ofNullable(beanWrapper.getPropertyValue(attributeName));



            //值为空进行下一属性的遍历
            if(optionalValue.equals(Optional.empty())) continue;

            //String类型查询
            if (attribute.getJavaType().equals(String.class) && doString(root, cb, predicates, attribute, optionalValue)) continue;

            //其他类型查询（除String类型）
            cb.equal(root.get(attribute),optionalValue.get());
        }
        //传入对象的等值和模糊匹配查询 属性遍历===>>end




        //predicates转为单个Predicate
        Predicate[] array = predicates.toArray(new Predicate[0]);
        //暂时全部是and条件
        return cb.and(array);
    }

    /**
     * 处理String类型的条件构造
     * @param root
     * @param cb
     * @param predicates 条件构造数组
     * @param attribute SingularAttribute 属性
     * @param optionalValue 对象属性值
     * @return
     */
    private  boolean doString(Root<T> root, CriteriaBuilder cb, List<Predicate> predicates, SingularAttribute attribute, Optional<Object> optionalValue) {
        String strValue = (String) optionalValue.get();
        //字符串判空
        if (strValue != ""){
            Expression<String> expression = root.get(attribute);
            //模糊查询
            if (strValue.endsWith(LIKE_SUFFIX)){
                if (strValue.startsWith(LIKE_PREFIX)){
                    predicates.add(cb.like(expression,("%"+strValue.substring(5,strValue.length()-2)+"%")));
                    return true;
                }
                if (strValue.startsWith(LIKE_IGNORE_CASE_PREFIX)){
                    expression = cb.lower(expression);
                    predicates.add(cb.like(expression,("%"+strValue.toLowerCase().substring(5,strValue.length()-2))+"%"));
                    return true;
                }
            }else{
                //字符串等值查询
                Predicate equal = cb.equal(expression, strValue);
                predicates.add(equal);
                return true;
            }
        }
        return false;
    }



    public <T> T convert(String value, Class<T> expectedClass) {
       if (expectedClass.isAssignableFrom(Date.class)) {
           return (T) convertToDate(value);
       }else if (isAssignableFromAnyOf(expectedClass, Boolean.class, boolean.class)) {
           return (T) convertToBoolean(value);
       } else if (isAssignableFromAnyOf(expectedClass, Integer.class, int.class, Long.class, long.class)) {
           return (T) convertToLong(value);
       } else if (isAssignableFromAnyOf(expectedClass, float.class, Float.class)) {
           return (T) convertToFloat(value);
       } else if (isAssignableFromAnyOf(expectedClass, double.class, Double.class)) {
           return (T) convertToDouble(value);
       } else if (expectedClass.isAssignableFrom(LocalDateTime.class)) {
           return (T) convertToLocalDateTime(value);
       } else if (expectedClass.isAssignableFrom(LocalDate.class)) {
           return (T) convertToLocalDate(value);
       } else if (expectedClass.isAssignableFrom(BigDecimal.class)) {
           return (T) convertToBigDecimal(value);
       } else if (expectedClass.isAssignableFrom(UUID.class)) {
           return (T) convertToUUID(value);
       } else if (expectedClass.isAssignableFrom(OffsetDateTime.class)) {
           return (T) convertToOffsetDateTime(value);
       } else if (expectedClass.isAssignableFrom(Instant.class)) {
           return (T) convertToInstant(value);
       }
        return (T) value;
    }

    private boolean isAssignableFromAnyOf(Class<?> expectedClass, Class<?>... candidates) {
        for (Class<?> candidate : candidates) {
            if (expectedClass.isAssignableFrom(candidate)) {
                return true;
            }
        }
        return false;
    }

    private LocalDate convertToLocalDate(String value) {
        String dateFormat = getDateFormat(LocalDate.class);
        try {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
            return LocalDate.parse(value, formatter);
        } catch (DateTimeParseException | IllegalArgumentException e) {
            throw new RuntimeException(value+"LocalDate format exception, expected format: " + dateFormat);
        }
    }

    private LocalDateTime convertToLocalDateTime(String value) {
        String dateFormat = getDateFormat(LocalDateTime.class);
        try {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
            return LocalDateTime.parse(value, formatter);
        } catch (DateTimeParseException | IllegalArgumentException e) {
            throw new RuntimeException(value+ "LocalDateTime format exception, expected format:" + dateFormat);
        }
    }

    private Long convertToLong(String value) {
        try {
            return Long.valueOf(value);
        } catch (NumberFormatException e) {
            throw new RuntimeException(value+ "number format exception");
        }
    }

    private Double convertToDouble(String value) {
        try {
            return Double.valueOf(value);
        } catch (NumberFormatException e) {
            throw new RuntimeException(value+ "number format exception");
        }
    }

    private Float convertToFloat(String value) {
        try {
            return Float.valueOf(value);
        } catch (NumberFormatException e) {
            throw new RuntimeException(value+ "number format exception");
        }
    }

    private BigDecimal convertToBigDecimal(String value) {
        try {
            return new BigDecimal(value);
        } catch (NumberFormatException e) {
            throw new RuntimeException(value+ "number format exception");
        }
    }

    private Boolean convertToBoolean(String value) {
        if ("true".equals(value)) {
            return true;
        } else if ("false".equals(value)) {
            return false;
        } else {
            throw new RuntimeException(value+ "unparseable boolean");
        }
    }

    public Date convertToDate(String value) {
        String dateFormat = getDateFormat(Date.class);
        try {
            return new SimpleDateFormat(dateFormat).parse(value);
        } catch (ParseException e) {
            throw new RuntimeException(value+ "Date format exception, expected format: " + dateFormat);
        }
    }

    public String getDateFormat(Class<?> clazz) {
        if (dateFormat == null) {
            return DEFAULT_DATE_FORMATS.get(clazz);
        }

        return dateFormat;
    }

    public UUID convertToUUID(String value) {
        try {
            return UUID.fromString(value);
        } catch (IllegalArgumentException e) {
            throw new RuntimeException(value+ "unparseable uuid");
        }
    }

    public OffsetDateTime convertToOffsetDateTime(String value) {
        String dateFormat = getDateFormat(OffsetDateTime.class);
        try {
            return OffsetDateTime.parse(value, DateTimeFormatter.ofPattern(dateFormat));
        } catch (DateTimeParseException | IllegalArgumentException e) {
            throw new RuntimeException(value+ "OffsetDateTime format exception, expected format: " + dateFormat);
        }
    }

    public Instant convertToInstant(String value) {
        String dateFormat = getDateFormat(Instant.class);
        try {
            return Instant.from(DateTimeFormatter.ofPattern(dateFormat).parse(value));
        } catch (DateTimeParseException | IllegalArgumentException e) {
            throw new RuntimeException(value + "Instant format exception, expected format: " + dateFormat);
        }
    }


}
